package actors

import akka.actor.typed.{ActorSystem, Behavior}
import akka.actor.typed.scaladsl.{ActorContext, Behaviors}

import scala.concurrent.Future
import scala.util.{Failure, Success}
import actors.Messages._
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.{ContentTypes, HttpEntity, HttpMethods, HttpRequest, HttpResponse}
import com.typesafe.config.{Config, ConfigFactory}
import slick.jdbc.PostgresProfile.api._
import database.Model._

import java.util.Date

object Device {

  import database.Connection

  val propsConfig: Config = ConfigFactory.load("propsConfiguration.properties")
  val deviceRestDomain: String = propsConfig.getString("deviceSideDomain")

  def registerNewDevice(newCamera: CameraDevice)(implicit ac: ActorContext[_]): Future[(Registration, CameraDevice)] = {
    import ac.executionContext
    implicit val as: ActorSystem[Nothing] = ac.system

    // Registration insertion
    val registrationInsertion =
      registrationTable.returning(registrationTable) += Registration(newCamera.id, newCamera.name)
    val registrationFuture: Future[Registration] =
      Connection.db.run(registrationInsertion)
    // Camera insertion
    val cameraInsertion =
      cameraDeviceTable.returning(cameraDeviceTable) += newCamera
    val cameraFuture: Future[CameraDevice] =
      Connection.db.run(cameraInsertion)

    // Step 1: POST to device side to register a camera
    val registerRequest: HttpRequest = HttpRequest(
      method = HttpMethods.POST,
      uri = s"$deviceRestDomain/camera/create_new_camera",
      entity = HttpEntity(
        ContentTypes.`application/x-www-form-urlencoded`,
        Map(
          "camera_num" -> 198,
          "camera_name" -> newCamera.name,
          "camera_path" -> "/dev/video0",
          "is_available" -> 1,
          "camera_manufacturer" -> "晟悦"
        ).mkString("&").split(" -> ").mkString("=")
      )
    )
    val deviceRegisterFuture: Future[HttpResponse] = Http().singleRequest(registerRequest)

    // Step 2: POST to device side to activate this camera
    val activateRequest: HttpRequest = HttpRequest(
      method = HttpMethods.POST,
      uri = s"$deviceRestDomain/camera/startCamera",
      entity = HttpEntity(
        ContentTypes.`application/x-www-form-urlencoded`,
        Map(
          "camera_num" -> 198,
          "rtmp_address" -> newCamera.streamURL
        ).mkString("&").split(" -> ").mkString("=")
      )
    )
    val deviceActivateFuture: Future[HttpResponse] = Http().singleRequest(activateRequest)

    // form Future res
    for {
      _ <- deviceRegisterFuture
      _ <- deviceActivateFuture
      registration <- registrationFuture
      camera <- cameraFuture
    } yield (registration, camera)
  }

  def getDeviceInfo(deviceId: String): Future[Option[CameraDevice]] =
    Connection.db.run(
      cameraDeviceTable
        .filter((_: CameraDeviceTable).id === deviceId)
        .result
        .headOption
    ) // select * from Camera where id like "$id"

  def updateDeviceInfo(updatedCamera: CameraDevice)(implicit ac: ActorContext[_]): Future[Int] = {
    import ac.executionContext
    implicit val as: ActorSystem[Nothing] = ac.system

    val configureRequest: HttpRequest = HttpRequest(
      method = HttpMethods.POST,
      uri = s"$deviceRestDomain/camera/config_camera",
      entity = HttpEntity(
        ContentTypes.`application/x-www-form-urlencoded`,
        Map(
          "camera_num" -> 198,
          "camera_width" -> updatedCamera.width,
          "camera_height" -> updatedCamera.height,
          "camera_bit_rate" -> updatedCamera.bitRate,
          "camera_frame_rate" -> updatedCamera.frameRate,
          "camera_frame_quality" -> updatedCamera.quality
        ).mkString("&").split(" -> ").mkString("=")
      )
    )
    val deviceConfigureFuture: Future[HttpResponse] = Http().singleRequest(configureRequest)

    for {
      _ <- deviceConfigureFuture
      dbUpdateResult <- Connection.db.run(
        cameraDeviceTable
          .filter((_: CameraDeviceTable).id === updatedCamera.id)
          .update(updatedCamera)
      )
    } yield dbUpdateResult
  }

  def unregisterDevice(deviceId: String)(implicit ac: ActorContext[_]): Future[(Int, Int)] = {
    import ac.executionContext
    implicit val as: ActorSystem[Nothing] = ac.system

    val deviceUnregisterFuture: Future[Int] = Connection.db.run(
      cameraDeviceTable
        .filter((_: CameraDeviceTable).id === deviceId)
        .delete
    )
    val registrationUnregisterFuture: Future[Int] = Connection.db.run(
      registrationTable
        .filter((_: RegistrationTable).id === deviceId)
        .delete
    )
    val deactivateRequest: HttpRequest = HttpRequest(
      method = HttpMethods.POST,
      uri = s"$deviceRestDomain/camera/stopCamera",
      entity = HttpEntity(
        ContentTypes.`application/x-www-form-urlencoded`,
        Map(
          "camera_num" -> 198,
        ).mkString("&").split(" -> ").mkString("=")
      )
    )
    val deviceDeactivateFuture: Future[HttpResponse] = Http().singleRequest(deactivateRequest)

    for {
      _ <- deviceDeactivateFuture
      cameraUnregisterResult <- deviceUnregisterFuture
      registrationUnregisterFuture <- registrationUnregisterFuture
    } yield (cameraUnregisterResult, registrationUnregisterFuture)
  }

  def apply(newRegisteredDevice: CameraDevice): Behavior[DeviceProtocol] = active(newRegisteredDevice)

  def active(camera: CameraDevice): Behavior[DeviceProtocol] = {
    Behaviors.receive[DeviceProtocol] { (context: ActorContext[DeviceProtocol], message: DeviceProtocol) =>
      implicit val ac: ActorContext[DeviceProtocol] = context

      context.log.info(s"[${context.self.path.name}] Receive task: $message")

      message match {
        case DeviceRegisterTask(newDevice, replyTo) =>
          val registerFuture: Future[(Registration, CameraDevice)] = registerNewDevice(newDevice)
          context.pipeToSelf(registerFuture) {
            case Success((_, cameraDevice)) => ReplyTask(DeviceRegisteredResponse(cameraDevice), replyTo)
            case Failure(exception) => ReplyTask(RegistrationFailure(exception), replyTo)
          }

          Behaviors.same
        case DeviceGetInfoTask(replyTo) =>
          val getInfoFuture: Future[Option[CameraDevice]] = getDeviceInfo(camera.id)
          context.pipeToSelf(getInfoFuture) {
            case Success(result) =>
              result match {
                case Some(camera) =>
                  ReplyTask(GetDeviceInfoResponse(camera), replyTo)
                case None =>
                  ReplyTask(ExecutionFailed(new RuntimeException("No such device stored in database.")), replyTo)
              }
            case Failure(exception) => ReplyTask(ExecutionFailed(exception), replyTo)
          }

          Behaviors.same
        case DeviceUpdateInfoTask(property, value, replyTo) =>
          // field modification
          val couldBeUpdatedCamera: Either[CameraDevice, Throwable] = property match {
            case "name" => Left(camera.copy(name = value))
            case "width" => Left(camera.copy(width = value.toInt))
            case "height" => Left(camera.copy(height = value.toInt))
            case "frameRate" => Left(camera.copy(frameRate = value.toInt))
            case "bitRate" => Left(camera.copy(bitRate = value.toInt))
            case "manufacturer" => Left(camera.copy(manufacturer = value))
            case "quality" => Left(camera.copy(quality = value))
            case "status" => Left(camera.copy(status = value))
            case "onlineTime" =>
              // insertion
              Left(camera.copy(onlineTime = (new Date).toString))
            case "offlineTime" =>
              // insertion
              Left(camera.copy(offlineTime = (new Date).toString))
            case "id" =>
              Right(new RuntimeException("Device id cannot be modified."))
            case _ =>
              Right(new RuntimeException("Illegal property type for modification"))
          }

          couldBeUpdatedCamera match {
            case Left(updatedCamera) =>
              val updateInfoFuture: Future[Int] = updateDeviceInfo(updatedCamera)
              context.pipeToSelf(updateInfoFuture) {
                case Success(_) => ReplyTask(DeviceInfoUpdatedResponse(updatedCamera), replyTo)
                case Failure(exception) => ReplyTask(ExecutionFailed(exception), replyTo)
              }
            case Right(propertyIllegalException) =>
              context.log.error(s"[${context.self.path.name}] ${propertyIllegalException.getMessage}")
              context.pipeToSelf(Future.failed(propertyIllegalException)) {
                case Failure(exception) => ReplyTask(ExecutionFailed(exception), replyTo)
              }
          }

          Behaviors.same
        case DeviceUnregisterTask(id, replyTo) =>
          val unregisterFuture: Future[(Int, Int)] = unregisterDevice(id)
          context.pipeToSelf(unregisterFuture) {
            case Success(_) => ReplyTask(DeviceUnregisterResponse, replyTo)
            case Failure(exception) => ReplyTask(ExecutionFailed(exception), replyTo)
          }

          Behaviors.same
        case ReplyTask(ack, replyTo) =>
          context.log.info(s"[${context.self.path.name}] Reply ACK: $ack")
          replyTo ! ack

          ack match {
            case RegistrationFailure(_) | DeviceUnregisterResponse =>
              Behaviors.stopped
            case DeviceInfoUpdatedResponse(updatedCamera) =>
              active(updatedCamera)
            case _ =>
              Behaviors.same
          }
      }
    }
  }
}
